package com.storyroad.utils;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;

import com.storyroad.responseClasses.StoryView;
import com.storyroad.utils.Utilities;

/**
 * Creates recommendations for the given user according to his previous ratings.
 */
public class RecommendationGenerator {
	/** Connection to the database */
	private Connection conn;
	
	/** Username to generate the recommendations */
	private String username;

	public RecommendationGenerator(Connection conn, String username) {
		this.conn = conn;
		this.username = username;
	}

	/**
	 * @return Recommendations generated by the class.
	 */
	public ArrayList<StoryView> generateRecommendations() {

		ArrayList<StoryRatingPair> ratedStories = getRatedStories();
		ArrayList<TagRatingPair> tagPoints = generateTagPoints(ratedStories);
		ArrayList<StoryPointPair> storyPoints = generateStoryPoints(tagPoints);
		ArrayList<StoryView> finalStories = generateStoryViews(storyPoints);
		return finalStories;
	}

	/**
	 * @return Stories rated by the user.
	 */
	private ArrayList<StoryRatingPair> getRatedStories() {

		int userId = Utilities.getUserId(conn, username);
		ArrayList<StoryRatingPair> result = new ArrayList<StoryRatingPair>();

		try {
			PreparedStatement stmt = conn
					.prepareStatement("SELECT s_id, rating FROM "
							+ TableNames.RATING_TABLE + " WHERE u_id=?");
			stmt.setInt(1, userId);
			ResultSet resultSet = stmt.executeQuery();
			while (resultSet.next()) {
				int s_id = resultSet.getInt("s_id");
				int rating = resultSet.getInt("rating");
				result.add(new StoryRatingPair(s_id, rating));
			}
		} catch (SQLException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}

		return result;
	}

	/**
	 * @param stories Stories rated by the user.
	 * @return Points for the tags which is contained in the stories rated by the user.
	 */
	private ArrayList<TagRatingPair> generateTagPoints(
			ArrayList<StoryRatingPair> stories) {

		ArrayList<TagRatingPair> result = new ArrayList<TagRatingPair>();

		HashMap<String, RatingCountPair> tagPointMap = new HashMap<String, RatingCountPair>();
		
		
		for (StoryRatingPair storyRatingPair : stories) {
			int currentRating = storyRatingPair.rating;
			ArrayList<String> tags = getTags(storyRatingPair.storyId);
			for (String tag : tags) {
				if (tagPointMap.containsKey(tag)) {
					RatingCountPair rcp = tagPointMap.get(tag);
					rcp.count++;
					//If the story already has a rating, taking the average closer to the upper limit.
					rcp.rating = (rcp.rating + currentRating)
							/ (rcp.count - 0.2);
					tagPointMap.put(tag, rcp);
				} else {
					RatingCountPair rcp = new RatingCountPair(currentRating, 1);
					tagPointMap.put(tag, rcp);
				}
			}
		}

		for (Iterator<String> itr = tagPointMap.keySet().iterator(); itr
				.hasNext();) {
			String tag = itr.next();
			double rating = (tagPointMap.get(tag)).rating;
			//If the rating has a point more than 5, it is added to the result.
			if (rating > 5) {
				result.add(new TagRatingPair(tag, rating));
			}
		}
		
		//Sorting the result array using the rating field of the object.
		Collections.sort(result, new Comparator<TagRatingPair>() {
			public int compare(TagRatingPair left, TagRatingPair right) {
				if ((left.rating - right.rating) > 0) {
					return 1;
				} else {
					return -1;
				}
			}
		});

		Collections.reverse(result);
		return result;
	}

	/**
	 * @param tagPoints Points for the tags which is contained in the stories rated by the user.
	 * @return Points for the stories generated using tag points.
	 */
	private ArrayList<StoryPointPair> generateStoryPoints(
			ArrayList<TagRatingPair> tagPoints) {

		ArrayList<StoryPointPair> result = new ArrayList<StoryPointPair>();
		HashMap<Integer, Double> storyPointMap = new HashMap<Integer, Double>();
		for (TagRatingPair tagRatingPair : tagPoints) {
			double currentPoint = tagRatingPair.rating;
			ArrayList<Integer> taggedStories = findStories(tagRatingPair.tag);
			//Generating story points using the points of tags they contain.
			for (Integer storyId : taggedStories) {
				if (storyPointMap.containsKey(storyId)) {
					storyPointMap.put(storyId, storyPointMap.get(storyId)
							+ currentPoint);
				} else {
					storyPointMap.put(storyId, currentPoint);
				}
			}
		}
		
		for (Iterator<Integer> itr = storyPointMap.keySet().iterator(); itr
				.hasNext();) {
			Integer story = itr.next();
			double point = storyPointMap.get(story);
			result.add(new StoryPointPair(story, point));
		}
		
		//Sorting the result list using the point field of the object.
		Collections.sort(result, new Comparator<StoryPointPair>() {
			public int compare(StoryPointPair left, StoryPointPair right) {
				if ((left.point - right.point) > 0) {
					return 1;
				} else {
					return -1;
				}
			}
		});

		Collections.reverse(result);
		return result;
	}

	/**
	 * @param storyPoints Points for the stories generated using by tag points.
	 * @return StoryViews for stories to recommend the user.
	 */
	private ArrayList<StoryView> generateStoryViews(
			ArrayList<StoryPointPair> storyPoints) {

		ArrayList<StoryView> result = new ArrayList<StoryView>();

		for (StoryPointPair storyPointPair : storyPoints) {
			if (!isAlreadyRated(storyPointPair.storyId)
					&& !isOwnStory(storyPointPair.storyId)) {
				StoryView storyView = Utilities.generateStoryView(conn,storyPointPair.storyId);
				result.add(storyView);
			}
		}

		return result;
	}

	/**
	 * @param s_id Id of the desired story.
	 * @return Tags of the given story.
	 */
	private ArrayList<String> getTags(int s_id) {

		ArrayList<String> result = new ArrayList<String>();

		try {
			PreparedStatement stmt = conn.prepareStatement("SELECT tag FROM "
					+ TableNames.TAGS_TABLE + " WHERE s_id=?");

			stmt.setInt(1, s_id);
			ResultSet resultSet = stmt.executeQuery();
			while (resultSet.next()) {
				result.add(resultSet.getString("tag"));
			}
		} catch (SQLException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}

		return result;
	}

	/**
	 * @param tag A semantic tag.
	 * @return Stories that contain the given tag.
	 */
	private ArrayList<Integer> findStories(String tag) {

		ArrayList<Integer> result = new ArrayList<Integer>();

		try {
			PreparedStatement stmt = conn.prepareStatement("SELECT s_id FROM "
					+ TableNames.TAGS_TABLE + " WHERE tag=?");

			stmt.setString(1, tag);
			ResultSet resultSet = stmt.executeQuery();
			while (resultSet.next()) {
				result.add(resultSet.getInt("s_id"));
			}
		} catch (SQLException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}

		return result;
	}

	/**
	 * @param storyId Id of a story.
	 * @return If user already rated this story or not.
	 */
	private boolean isAlreadyRated(int storyId) {

		boolean alreadyRated = false;

		try {
			PreparedStatement stmt = conn.prepareStatement("SELECT * FROM "
					+ TableNames.RATING_TABLE + " WHERE s_id=?");

			stmt.setInt(1, storyId);
			ResultSet resultSet = stmt.executeQuery();
			if (resultSet.next()) {
				alreadyRated = true;
			}
		} catch (SQLException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}

		return alreadyRated;
	}

	/**
	 * @param storyId Id of a story.
	 * @return If the user owns this story or not.
	 */
	private boolean isOwnStory(int storyId) {

		boolean isOwn = false;

		try {
			PreparedStatement stmt = conn.prepareStatement("SELECT owner FROM "
					+ TableNames.STORIES_TABLE + " WHERE s_id=?");

			stmt.setInt(1, storyId);
			ResultSet resultSet = stmt.executeQuery();
			if (resultSet.next()) {
				int userId = resultSet.getInt("owner");
				if (this.username.equals(Utilities.getUsername(conn, userId))) {
					isOwn = true;
				}
			}
		} catch (SQLException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}

		return isOwn;
	}

	/** Class holding story and their rating given by the user.
	 *
	 */
	private class StoryRatingPair {

		private int storyId;
		private int rating;

		public StoryRatingPair(int storyId, int rating) {
			this.storyId = storyId;
			this.rating = rating;
		}
	}

	/** Class holding tag and their average rating given by the user.
	 *
	 */
	private class TagRatingPair {

		private String tag;
		private double rating;

		public TagRatingPair(String tag, double rating) {

			this.tag = tag;
			this.rating = rating;
		}
	}

	/** Class holding rating and their counts.
	 *
	 */
	private class RatingCountPair {

		private double rating;
		private int count;

		public RatingCountPair(int rating, int count) {

			this.rating = rating;
			this.count = count;
		}
	}

	/** Class holding storyId and their relative point generated using tag points.
	 *
	 */
	private class StoryPointPair {

		private int storyId;
		private double point;

		public StoryPointPair(int storyId, double point) {
			this.storyId = storyId;
			this.point = point;
		}

	}

}
